第7.3节 朴素贝叶斯实现
各位朋友大家好,欢迎来到月来客栈,我是掌柜空字符。
本期推送内容目录如下,如果本期内容对你有所帮助,欢迎点赞、转发支持掌柜!
7.3 朴素贝叶斯实现 7.3.1 特征计数实现 7.3.2 先验概率实现 7.3.3 条件概率实现 7.3.4 模型拟合实现 7.3.5 后验概率实现 7.3.6 使用示例 7.3.7 小结 引用
7.3 朴素贝叶斯实现
经过前面两个小节内容的介绍,对于朴素贝叶斯算法的原理我们已经有了清晰的认识。在本节内容中,笔者将开始分步对各个部分的实现进行详细地介绍。同时,需要说明的是以下实现代码均参考自sklearn 0.24.0 中的CategoricalNB
模块,只是对部分处理逻辑进行了修改与简化,完整代码见Book/utils/C01_naive_bayes_category.py
文件。
7.3.1 特征计数实现
通过第7.1节的内容可知,不管是计算先验概率还是条件概率,在这之前都需要先统计训练集中各个样本及样本特征取值的分布情况。因此,这里首先需要初始化相关的计数器;然后再对样本和特征样本特征取值的分布情况进行统计。
具体地,对于计数器的初始化工作实现代码如下所示:
1 class MyCategoricalNB(object):
2
3 def __init__(self, alpha=1.0):
4 self.alpha = alpha
5
6 def _init_counters(self):
7 self.class_count_ = np.zeros(self.n_classes, dtype=np.float64)
8 self.category_count_ = [np.zeros((self.n_classes, 0))
9 for _ in range(self.n_features_)]
在上述代码中,第3~4行是初始化平滑项系数alpha
。第7行class_count_
被初始化成了一个形状为[n_classes,]
的全零向量,其中n_classes
表示分类的类别数量,而每个维度分别表示每个类别的样本数量(例如[2,2,3]
表示0,1,2这三个类别的样本数分别是2,2,3),其目的是后续用于计算每个类别的先验概率。第8行category_count_
被初始化成了一个包含有n_features_
个元素的列表,其中n_features_
表示数据集的特征维度数量,同时category_count_
中每个元素的形状是[n_classes,0]
(后续每个元素将会更新为[n_classes,len(X_i)]
的形状,len(X_i)
表示X_i
这个特征的取值情况数量);而category_count_
的作用是记录在各个类别下每个特征变量中各种取值情况的数量,例如category_count_[i][j][k]
为10表示含义就是特征i
在类别j
下特征取值为k
的样本数量为10个。
在初始化两个计数器之后,进一步便可以实现各个类别及特征分布的统计,代码如下:
1 def _count(self, X, Y):
2 def _update_cat_count(X_feature, Y, cat_count, n_classes):
3 for j in range(n_classes): # 遍历每个类别
4 mask = Y[:, j].astype(bool) # 取每个类别下对应样本的索引
5 counts = np.bincount(X_feature[mask]) # 统计当前类别下,特征X_feature中各个取值下的数量
6 indices = np.nonzero(counts)[0]
7 cat_count[j, indices] += counts[indices]
8
9 self.class_count_ += Y.sum(axis=0) # Y: shape(n,n_classes) Y.sum(): shape(n_classes,)
10 self.n_categories_ = X.max(axis=0) + 1
11
12 for i in range(self.n_features_): # 遍历每个特征
13 X_feature = X[:, i] # 取每一列的特征
14 self.category_count_[i] = np.pad(self.category_count_[i],
15 [(0, 0), (0, self.n_categories_[i])],
16 'constant')
17 _update_cat_count(X_feature, Y,self.category_count_[i],self.n_classes)
在上述代码中,第1行参数Y
是原始标签经过one-hot编码后的形式,例如3分类问题中类别1会被编码成[0,1,0]
的形式,因此Y
的形状为[n,n_classes]
;第9行代码是计算得到每个类别对应的样本数量;第10行则是统计每个特征维度的取值数量(因为特征取值是从0开始的所以后面加了1),例如[3 3 3 3]
表示四个特征维度的取值均有3种情况;第12~13行开始遍历每个特征并取对应的特征列;第14~16行是对category_count_
中的每个元素填充self.n_categories_[i]
列全0向量,此时category_count_
中每个元素将会变成形状为[n_classes,len(X_i)]
的全零矩阵;第17行则是根据输入的每一列特征等相关参数来更新category_count_
计数器。
第3~5行为遍历每一个样本类别,并取每个类别下对应样本的索引,同时统计当前类别下特征列X_feature
中各个取值下的数量。同时,第5行中np.bincount
的作用的是统计每个值出现的次数,例如:
1 counts = np.bincount(np.array([0, 3, 5, 1, 4, 4]))
2 print(counts) # [1 1 0 1 2 1]
3 # 表示[0, 3, 5, 1, 4, 4]中0,1,2,3,4,5这个6个值的出现的频次分别是1,1,0,1,2,1
第6~7行则是用来更新cat_count
中当前输入特征每种取值情况的分布数量,例如cat_count[i,k]
表示的是第i
个类别下,特征X_feature
的第k
个取值的数量。
例如对于表7-1中的数据集来说,其对应的category_count_
计数器的结果为:
1 [[[4., 1.],[3., 7.]],
2 [[4., 1.],[4., 6.]],
3 [[1., 1., 3.],[2., 3., 5.]]]
其中[[4., 1.],[3., 7.]]
分别表示的含义就是:对于第1个特征来说,在y=0
这个类别下,取值为0的情况有4种(表中第4-7号样本),取值为1的情况有1种(第14号样本);在y=1
这个类别下,取值为0的情况有3种种(第1~3号样本),取值为1的情况有7种。
同理,对于第2个特征来说,在y=0
这个类别下,取值为0的情况有4种,取值为1的情况有1种;在y=1
这个类别下,取值为0的情况有4种,取值为1的情况有6种。
到此,对于数据集中样本及特征分布情况的计数就算是统计完了,接下来开始实现计算先验概率和条件概率。
7.3.2 先验概率实现
有了数据集中各个样本的分布情况后,计算先验概率就变得十分简单了,代码如下:
1 def _update_class_prior(self):
2 self.class_prior_ = (self.class_count_ + self.alpha) / # shape: [n_classes, ]
3 (self.class_count_.sum() + self.n_classes * self.alpha)
在上述代码中,第2~3行便是用来计算各个类别的先验概率,其中带有alpha
的地方便是第7.2.1节内容中的平滑处理项。
7.3.3 条件概率实现
进一步,可以根据category_count_
中的统计结果来实现各特征取值的条件概率,代码如下:
1 def _update_feature_prob(self):
2 feature_prob = []
3 for i in range(self.n_features_): # 遍历 每一个特征
4 smoothed_cat_count = self.category_count_[i] + self.alpha
5 smoothed_class_count = self.category_count_[i].sum(axis=1) +
6 self.category_count_[i].shape[1] * self.alpha
7 cond_prob = smoothed_cat_count / smoothed_class_count.reshape(-1, 1)
8 feature_prob.append(cond_prob)
9 self.feature_prob_ = feature_prob
在上述代码中,第4行是取当前特征在不同类别下各个特征的取值的统计结果,并加上平滑项系数;第5~6行是计算在当前特征下各类别样本的总数,并进行平滑处理;第7行则是计算每个特征取值对应的条件概率;第9行中feature_prob_
为一个包含有n_features_
个元素的列表,每个元素的shape
为 [self.n_classes, 特征取值数]
,因此feature_prob_[i][j][k]
表示的就是特征i
在类别j
下取值为k
时的概率。
例如对于表7-1中的训练数据集来说,计算得到feature_prob_
的结果如下所示:
[[0.8 0.2] [[0.8 0.2] [[0.2 0.2 0.6]
[0.3 0.7]] [0.4 0.6]] [0.2 0.3 0.5]]
上述结果中,第1个元素表示:对于第1个特征来说,在y=0
这个类别下,的概率为,的概率为;对于第1个特征来说,在y=1
这个类别下,的概率为,的概率为。这一结果也可以同第7.1.3节中的计算结果进行对比。
7.3.4 模型拟合实现
上述先验概率和条件概率的计算过程对应便是整个模型的拟合过程,换句话说对于贝叶斯算法来说,所谓的模型拟合就是计算先验概率和条件概率。在实现这部分代码之后,再通过一个函数将整个过程串起来即可,实现代码如下所示:
1 def fit(self, X, y):
2 self.n_features_ = X.shape[1]
3 labelbin = LabelBinarizer()
4 Y = labelbin.fit_transform(y)
5 self.classes_ = labelbin.classes_
6 if Y.shape[1] == 1:
7 Y = np.concatenate((1 - Y, Y), axis=1)
8 self.n_classes = Y.shape[1]
9 self._init_counters() # 初始化计数器
10 self._count(X, Y) # 对各个特征的取值情况进行计数,以计算条件概率等
11 self._update_class_prior()
12 self._update_feature_prob()
13 return self
在上述代码中,第2~4行用来将原始[0,1,2,3...]
这样的标签转换为one-hot编码形式的标签值,形状为[n,n_classes]
;第5行用来记录原始的标签类别,形状为[n_classes,]
;第6~7行用来处理当数据集为二分类时fit_transform
处理后的结果并不是one-hot形式,需要添加一列来转换成one-hot形式;第8行是获取数据集的类别数量;第9~12行则分别是上面3节内容介绍到的初始化计数器、特征取值分布情况统计、计算先验概率和计算条件概率。
7.3.5 后验概率实现
在完成模型的拟合过程后,对于新输入的样本来说其最终的预测结果则取决于对应的极大后验概率。实现代码如下所示: